package top.maof.book.search;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.wltea.analyzer.lucene.IKAnalyzer;
import top.maof.book.annotation.FieldType;
import top.maof.book.annotation.Key;
import top.maof.book.exception.NoKeyException;

import java.io.File;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.CountDownLatch;

@Slf4j
@Deprecated
@NoArgsConstructor
public class Search {
    // 创建目录对象
    private Directory directory;
    // 创建配置对象
    private IndexWriterConfig conf;
    // 创建索引写出工具
    private IndexWriter indexWriter;

    private IndexReader indexReader;

    // IK分词器
    private Analyzer analyzer = new IKAnalyzer();


    public Search(String path) throws Exception {
        this.directory = FSDirectory.open(new File(path).toPath());
    }


    public void startRead() throws Exception {
        this.indexReader = DirectoryReader.open(directory);
    }

    public void startWrite() throws Exception {
        this.conf = new IndexWriterConfig(this.analyzer);
        conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
        this.indexWriter = new IndexWriter(directory, conf);
    }

    //@Scheduled
    public void commit() throws Exception {
        this.indexWriter.commit();
        this.indexWriter.close();
    }

    public void close() throws Exception {
        indexWriter.close();
        indexReader.close();
    }

    /**
     * 创建单个索引
     *
     * @param t
     * @param <T>
     * @throws Exception
     */
    public <T> void write(T t) throws Exception {
        log.info(t.toString());
        this.indexWriter.addDocument(getDocument(t));
    }

    /**
     * 创建多个索引
     *
     * @param list
     * @param <E>
     * @throws Exception
     */
    public <E> void write(List<E> list) throws Exception {
        if (list == null || list.size() == 0) return;
        for (E e : list) {
            this.write(e);
        }
    }

    /**
     * 根据主键更新文档
     *
     * @param t
     * @param <T>
     * @return
     */
    public <T> void update(T t) throws Exception {
        Document document = getDocument(t);
        if (document == null) return;
        if (t == null) return;
        Class clazz = t.getClass();
        Field[] fields = clazz.getDeclaredFields();
        if (fields == null || fields.length == 0) return;
        //设置主键和对应值变量,更新需要主键
        String keyName = null;
        String keyValue = null;
        for (Field field : fields) {
            field.setAccessible(true);
            //获取主键值和名
            Key key = field.getAnnotation(Key.class);
            if (key != null) {
                keyName = clazz.getName() + "." + field.getName();
                keyValue = field.get(t) + "";
            } else
                return;
        }
        if (keyName == null || keyValue == null) throw new NoKeyException();
        this.indexWriter.updateDocument(new Term(keyName, keyValue), document);
    }


    private <T> Document getDocument(T t) throws Exception {
        if (t == null) return null;
        Class clazz = t.getClass();
        Field[] fields = clazz.getDeclaredFields();
        if (fields == null || fields.length == 0) return null;
        boolean hasKey = false;
        Document document = new Document();
        for (Field field : fields) {
            field.setAccessible(true);
            //获取主键值和名
            Key key = field.getAnnotation(Key.class);

            if (key != null) {
                hasKey = true;
            }
            FieldType fieldType = field.getAnnotation(FieldType.class);
            if (fieldType != null) {
                String value = String.valueOf(field.get(t));
                //name = 全类名.字段名
                String name = clazz.getName() + "." + field.getName();
                if (fieldType.isIndexed() && fieldType.isTokenized()) {
                    //索引且分词  TextField(FieldName,FieldValue, Store.NO)
                    IndexableField indexableField = fieldType.isStored() ? new TextField(name, value, org.apache.lucene.document.Field.Store.YES) : new TextField(name, value, org.apache.lucene.document.Field.Store.NO);
                    document.add(indexableField);
                    continue;
                } else if (fieldType.isIndexed() && !fieldType.isTokenized()) {
                    //只索引但不分词 StringField(FieldName, FieldValue,Store.YES))
                    IndexableField indexableField = fieldType.isStored() ? new StringField(name, value, org.apache.lucene.document.Field.Store.YES) : new StringField(name, value, org.apache.lucene.document.Field.Store.NO);
                    document.add(indexableField);
                    continue;
                } else if (!fieldType.isIndexed() && !fieldType.isTokenized() && fieldType.isStored()) {
                    document.add(new StoredField(name, value));
                    continue;
                }
            }
        }
        if (hasKey) return document;
        return null;
    }

    /**
     * 根据主键删除文档
     *
     * @param t
     * @param <T>
     * @throws Exception
     */
    public <T> void delete(T t) throws Exception {
        if (t == null) return;
        Class clazz = t.getClass();
        Field[] fields = clazz.getDeclaredFields();
        if (fields == null || fields.length == 0) return;
        String keyName = null, keyValue = null;
        Document document = new Document();
        for (Field field : fields) {
            field.setAccessible(true);
            //获取主键值和名
            Key key = field.getAnnotation(Key.class);
            if (key != null) {
                keyName = clazz.getName() + "." + field.getName();
                keyValue = field.get(t) + "";
                break;
            }
        }
        if (keyName == null || keyValue == null) throw new NoKeyException("无主键或主键为null");
        this.indexWriter.deleteDocuments(new Term(keyName, keyValue));
    }

    /**
     * 获取10条查询结果集合
     *
     * @param clazz   文本对应的pojo实体类的class
     * @param keyword 搜索关键字
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> search(Class<T> clazz, String keyword) throws Exception {
        return this.search(clazz, keyword, 10);
    }

    /**
     * 获取查询结果集合
     *
     * @param clazz   文本对应的pojo实体类的class
     * @param keyword 搜索关键字
     * @param n       结果条数
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> search(Class<T> clazz, String keyword, int n) throws Exception {
        // Class clazz = t.getClass();
        Field[] fields = clazz.getDeclaredFields();
        if (fields == null || fields.length == 0)
            return null;

        CountDownLatch countDownLatch = new CountDownLatch(fields.length);

        Set<QueryScoreDoc> set = Collections.synchronizedSet(new TreeSet<QueryScoreDoc>());

        for (Field field : fields) {
            field.setAccessible(true);
            // 字段不索引,直接下一个
            FieldType fieldType = field.getAnnotation(FieldType.class);
            if (fieldType == null || !fieldType.isIndexed()) {
                countDownLatch.countDown();
                continue;
            }

            Key key = field.getAnnotation(Key.class);
            String fieldName = clazz.getName() + "." + field.getName();

            // 字段为主键
            if (key != null) {
                Query query = new TermQuery(new Term(fieldName, keyword));

                SearchPool.get().execute(() -> {
                    try {
                        ScoreDoc[] scoreDocs = doSearch(query, 1);
                        if (scoreDocs != null && scoreDocs.length > 0) {
                            scoreDocs[0].score = (float) (scoreDocs[0].score * fieldType.weight());
                            set.add(new QueryScoreDoc(scoreDocs[0], query, fieldName));
                        }
                    } catch (Exception e) {
                    } finally {
                        countDownLatch.countDown();
                    }
                });
                continue;
            }

            SearchPool.get().execute(() -> {
                try {
                    Query query = new QueryParser(fieldName, analyzer).parse(keyword);
                    ScoreDoc[] scoreDocs = doSearch(query, n);
                    if (scoreDocs != null && scoreDocs.length > 0)
                        for (ScoreDoc scoreDoc : scoreDocs) {
                            scoreDoc.score = (float) (scoreDoc.score * fieldType.weight());
                            set.add(new QueryScoreDoc(scoreDoc, query, fieldName));
                        }

                } catch (Exception e) {
                } finally {
                    countDownLatch.countDown();
                }
            });

        }
        countDownLatch.await();

        return this.doHighlighter(set, clazz);
    }

    /**
     * 通过查询实现类和指定查询结果条数获取文本评分对象数组
     *
     * @param query 查询实现类
     * @param n     结果条数
     * @return
     * @throws Exception
     */
    private ScoreDoc[] doSearch(Query query, int n) throws Exception {
        // 索引搜索工具
        IndexSearcher searcher = new IndexSearcher(this.indexReader);
        // 搜索数据,两个参数：查询条件对象要查询的最大结果条数
        // 返回的结果是 按照匹配度排名得分前N名的文档信息（包含查询到的总条数信息、所有符合条件的文档的编号信息）。
        TopDocs topDocs = searcher.search(query, n);
        // 获取总条数
        //System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
        // 获取得分文档对象（ScoreDoc）数组.SocreDoc中包含：文档的编号、文档的得分
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        return scoreDocs;
    }


    public boolean isExist(String field, String id) throws Exception {
        Query query = new TermQuery(new Term(field, id));
        ScoreDoc[] scoreDocs = doSearch(query, 1);
        return scoreDocs != null && scoreDocs.length > 0;
    }

    /**
     * 高亮处理
     *
     * @param set   存放文本评分对象set集合
     * @param clazz 文本对应的pojo实体类
     * @param <T>
     * @return
     * @throws Exception
     */
    private <T> List<T> doHighlighter(Set<QueryScoreDoc> set, Class<T> clazz) throws Exception {
        //Field[] fields = t.getClass().getDeclaredFields();
        SimpleHTMLFormatter fors = new SimpleHTMLFormatter("<span style=\"color:red;\">", "</span>");
        List list = new ArrayList<T>();

        for (QueryScoreDoc queryScoreDoc : set) {
            T t1 = (T) clazz.newInstance();
            Document doc = this.indexReader.document(queryScoreDoc.scoreDoc.doc);

            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                String feildName = clazz.getName() + "." + field.getName();

                //主键无需高亮
                if (field.getAnnotation(Key.class) != null) {
                    field.set(t1, Integer.parseInt(doc.get(feildName)));
                    continue;
                }
                FieldType annotation = field.getAnnotation(FieldType.class);
                //不存储,直接跳过此字段
                if (annotation == null || !annotation.isStored()) {
                    continue;
                }
                //不索引,直接取值,无需高亮
                if (!feildName.equals(queryScoreDoc.field) || !annotation.isIndexed()) {
                    Class<?> type = field.getType();
                    if (type.newInstance() instanceof String)
                        field.set(t1, doc.get(feildName));
                    continue;
                }
                QueryScorer scorer = new QueryScorer(queryScoreDoc.query, feildName);
                Highlighter highlighter = new Highlighter(fors, scorer);
                Fragmenter fragment = new SimpleSpanFragmenter(scorer);
                highlighter.setTextFragmenter(fragment);
                TokenStream tokenStream = analyzer.tokenStream(feildName, new StringReader(doc.get(feildName)));
                field.set(t1, highlighter.getBestFragment(tokenStream, doc.get(feildName)));
            }
            list.add(t1);
        }
        return list;
    }

    /**
     * 搜索提示
     *
     * @param clazz
     * @param keyword
     * @return
     */
    public List<String> prompt(Class clazz, String keyword) throws Exception {
        return this.prompt(clazz, keyword, 5);
    }

    /**
     * 搜索提示
     *
     * @param clazz
     * @param keyword
     * @param n
     * @return
     */
    public List<String> prompt(Class clazz, String keyword, int n) throws Exception {
        Field[] fields = clazz.getDeclaredFields();
        if (fields == null || fields.length == 0) return null;
        CountDownLatch countDownLatch = new CountDownLatch(fields.length);
        Set<QueryScoreDoc> set = Collections.synchronizedSet(new TreeSet<>());
        for (Field field : fields) {
            field.setAccessible(true);
            //如果字段不索引或不存储或不分词,直接下一个
            if (field.getAnnotation(FieldType.class) == null || !field.getAnnotation(FieldType.class).isIndexed() ||
                    !field.getAnnotation(FieldType.class).isStored() || !field.getAnnotation(FieldType.class).isTokenized()) {
                countDownLatch.countDown();
                continue;
            }

            Key key = field.getAnnotation(Key.class);
            String fieldName = clazz.getName() + "." + field.getName();
            if (key != null) {
                countDownLatch.countDown();
                continue;
            }

            SearchPool.get().execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Query query = new QueryParser(fieldName, analyzer).parse(keyword);
                        ScoreDoc[] scoreDocs = doSearch(query, n);
                        if (scoreDocs != null && scoreDocs.length != 0)
                            for (ScoreDoc scoreDoc : scoreDocs)
                                if (set.size() < n)
                                    set.add(new QueryScoreDoc(scoreDoc, query, fieldName));
                    } catch (Exception e) {
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            });
        }
        countDownLatch.await();
        return this.promptHighlighter(set);
    }

    private List<String> promptHighlighter(Set<QueryScoreDoc> set) throws Exception {
        SimpleHTMLFormatter fors = new SimpleHTMLFormatter("<span style=\"color:red;\">", "</span>");
        List list = new ArrayList<String>();
        for (QueryScoreDoc queryScoreDoc : set) {
            Document doc = this.indexReader.document(queryScoreDoc.scoreDoc.doc);
            QueryScorer scorer = new QueryScorer(queryScoreDoc.query, queryScoreDoc.field);
            Highlighter highlighter = new Highlighter(fors, scorer);
            Fragmenter fragment = new SimpleSpanFragmenter(scorer);
            highlighter.setTextFragmenter(fragment);
            TokenStream tokenStream = analyzer.tokenStream(queryScoreDoc.field, new StringReader(doc.get(queryScoreDoc.field)));
            list.add(highlighter.getBestFragment(tokenStream, doc.get(queryScoreDoc.field)));
        }
        return list;
    }

}

